package pers.vic.upload.socket.service;

import cn.xuqiudong.common.base.model.BaseResponse;
import cn.xuqiudong.common.base.vo.BooleanWithMsg;
import cn.xuqiudong.common.util.JsonUtil;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.web.socket.BinaryMessage;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import pers.vic.upload.business.BusinessFacade;
import pers.vic.upload.constant.ResultEnum;
import pers.vic.upload.constant.CommandEnum;
import pers.vic.upload.socket.model.*;

import java.io.IOException;
import java.nio.ByteBuffer;

/**
 * 描述: 文件上传的handler的service
 * @author Vic.xu
 * @date 2022-06-09 9:33
 */
@Service
public class UploadHandlerService {
    /**
     * 当前session中的上传文件任务 key
     */
    public static final String CURRENT_UPLOAD_TASK = "current_upload_task";

    private static final Logger LOGGER = LoggerFactory.getLogger(UploadHandlerService.class);
    /**
     * 上传速度， 放在session中的key
     */
    private static final String UPLOAD_SPEED = "upload_speed";

    private final FileStorageService fileStorageService;

    private final BreakPointService breakPointService;

    private final BusinessFacade businessFacade;

    public UploadHandlerService(FileStorageService fileStorageService, BreakPointService breakPointService, BusinessFacade businessFacade) {
        this.fileStorageService = fileStorageService;
        this.breakPointService = breakPointService;
        this.businessFacade = businessFacade;
    }

    /**
     * 接收到字符串后的处理：一般应为初始化指令
     * @param session WebSocketSession
     * @param message TextMessage
     * @throws IOException ex
     */
    public void handleTextMessage(WebSocketSession session, TextMessage message) throws IOException {
        String payload = message.getPayload();
        //1. 把字符串转为指令对象
        InitCommandInfo info = JsonUtil.jsonToObject(payload, InitCommandInfo.class);
        JsonUtil.printJson(info);
        //2. 判断是否为正确的初始化指令
        if (info == null || info.getCommand() != CommandEnum.init) {
            LOGGER.error("不合法的操作指令");
            sendMsg(session, SocketResponse.error(ResultEnum.INVALID_command));
            return;
        }
        //3 合法性校验
        BaseResponse<?> check = businessFacade.check(info.getFilename(), info.getType(), true);
        if (!check.isSuccess()) {
            sendMsg(session, new SocketResponse(check.getCode(), check.getMsg()));
            return;
        }

        //4. 从redis中获取断点信息
        String identity = info.getIdentity(session);
        RealFile point = breakPointService.getBreakPoint(identity);
        // 5不存在断点，则重新上传
        if (point == null) {
            LOGGER.debug("不存在断点， 重新开始上传");
            initializeFile(info, session);
            return;
        }

        //6. 检查长度：当前已经上传的文件大小是否大于客户端传来的文件写入位置，
        long physicalLength = point.currentLength();
        if (physicalLength > info.getTotal()) {
            LOGGER.info("当前已上传到服务端的文件长度已大于最大长度，请重新上传");
            initializeFile(info, session);
            return;
        }
        //7. 长度相等，则完成：清除redis，告知客户端
        if (physicalLength == info.getTotal()) {
            //后续处理
            afterComplete(session, info, point);
            return;
        }
        //8. 续传：
        initializeResume(session, info, physicalLength, point);


    }

    /**
     * 接收到二进制内容后的处理：
     * @param session socket session
     * @param message BinaryMessage
     * @throws Exception ex
     */
    public void handleBinaryMessage(WebSocketSession session, BinaryMessage message) throws Exception {
        CurrentUploadTask task = (CurrentUploadTask) session.getAttributes().get(CURRENT_UPLOAD_TASK);
        if (task == null) {
            sendMsg(session, SocketResponse.error(ResultEnum.UNINITIALIZED_TASK));
            LOGGER.error("客户端请求异常，当前未初始化任务");
            return;
        }
        //必须在getPayload之前调用，否则读取到的长度为0
        int payloadLength = message.getPayloadLength();
        ByteBuffer payload = message.getPayload();
        boolean success = fileStorageService.upLoadToStorageByByteBuff(payload, task.getWritePosition(), task.getRealFile());
        if (!success) {
            LOGGER.error("写入分片内容错误");
            sendMsg(session, SocketResponse.error(ResultEnum.UPLOAD_SLICE_ERROR));
            return;
        }
        //写入后 修改写入位置
        task.increasePosition(payloadLength);
        //判断是否上传完毕，未上传完毕
        if (task.getWritePosition() <= task.getCommandInfo().getTotal()) {
            onProgress(session, task, payloadLength);
        }
        if (task.getWritePosition() == task.getCommandInfo().getTotal()) {
            afterComplete(session, task);
        } else if (task.getWritePosition() > task.getCommandInfo().getTotal()){
            //长度错误
            LOGGER.error("实际写入文件超出初始化命令中指定的长度");
            sendMsg(session, SocketResponse.error(ResultEnum.LENGTH_OVERFLOW));
        }

    }

    /**
     * 断点续传
     *   a. 初始化续传信息
     *   b.  存入session
     *   c. 存入redis
     * @param session WebSocketSession
     * @param info  CommandInfo
     * @param physicalLength 服务端已经上传的文件长度
     * @param realFile realFile
     */
    private void initializeResume(WebSocketSession session, InitCommandInfo info, long physicalLength, RealFile realFile) throws IOException {
        long position = Math.max(physicalLength, 0L);
        LOGGER.info("恢复断点续传, id={}, position={}", info.getIdentity(session), position);
        CurrentUploadTask task = CurrentUploadTask.resume(info, position, realFile);
        setCurrentUploadTask(session, task);
        //告知客户端 客户端已经准备好，且已经上传的长度
        SocketResponse response = SocketResponse.success(CommandEnum.ready, physicalLength);
        sendMsg(session, response);
        //更新进度按
        onProgress(session, task, 0);
    }

    /**
     * 把当前上传任务保存到session
     * @param session webscoket session
     * @param task 当前上传文件
     */
    private void setCurrentUploadTask(WebSocketSession session, CurrentUploadTask task) {
        Object oldTask = session.getAttributes().get(CURRENT_UPLOAD_TASK);
        if (oldTask != null) {
            LOGGER.info("移除历史上传文件任务{}", oldTask);
        }
        session.getAttributes().put(CURRENT_UPLOAD_TASK, task);
        //把断点信息保存到session
        RealFile realFile = task.getRealFile();
        String identity = task.getCommandInfo().getIdentity(session);
        breakPointService.saveBreakPoint(identity, realFile);


    }

    /**
     * 文件上传结束后的操作
     * @param session session
     * @param task CurrentUploadTask
     */
    private void afterComplete(WebSocketSession session, CurrentUploadTask task) throws IOException {
        LOGGER.debug("finish file upload {}, {}", task.getCommandInfo().getFilename(), task.getWritePosition());
        //告知客户端已经完成
        sendMsg(session, SocketResponse.success(CommandEnum.finished));
        //清理断点信息
        breakPointService.clear(task.getCommandInfo().getIdentity(session));
        // 清理当前任务
        session.getAttributes().remove(CURRENT_UPLOAD_TASK);
        //业务操作
        RealFile realFile = task.getRealFile();
        businessFacade.handlerBusiness(realFile);
        //删除掉文件通道中持有的文件
        FileChannelHolder.remove(realFile.file());
    }

    /**
     * 直接完成文件上传结束后的操作
     * @param session session
     * @param point RealFile
     */
    private void afterComplete(WebSocketSession session,InitCommandInfo info, RealFile point) throws IOException {
        LOGGER.debug("finish file upload {}, {}", info.getFilename(), point.currentLength());
        //告知客户端已经完成
        sendMsg(session, SocketResponse.success(CommandEnum.finished));
        //清理断点信息
        breakPointService.clear(info.getIdentity(session));
        // 清理当前任务
        session.getAttributes().remove(CURRENT_UPLOAD_TASK);
        businessFacade.handlerBusiness(point);
    }


    /**
     * 初始化一个文件，从头开始上传
     * @param info 初始化命令
     * @param session session
     */
    private void initializeFile(InitCommandInfo info, WebSocketSession session) throws IOException {
        LOGGER.debug("开始新文件上传,filename={}, total length = {}", info.getFilename(), info.getTotal());
        RealFile file;
        try {
            BaseResponse<RealFile> createFileResult = fileStorageService.createNewFile(info.getFilename(),info.getType());
            if (!createFileResult.isSuccess()) {
                //可能是： 当前文件上传中
                sendMsg(session, new SocketResponse(createFileResult.getCode(), createFileResult.getMsg()));
                return;
            }
            file = createFileResult.getData();
        } catch (Exception e) {
            LOGGER.error("创建新文件异常", e);
            //告知客户端异常
            sendMsg(session, SocketResponse.error(ResultEnum.CREATE_FILE_FAILURE));
            return;
        }
        if (info.getTotal() == 0) {
            LOGGER.debug("0 字节空文件特殊处理: 直接完成");
            fileStorageService.touch(file);
            //告知客户端处理完毕
            sendMsg(session, SocketResponse.success(CommandEnum.finished));
            return;
        }
        //非0字节文件
        CurrentUploadTask task = CurrentUploadTask.newTask(info, file);
        setCurrentUploadTask(session, task);
        //告知客户端开始从0处发送字节
        sendMsg(session, SocketResponse.success(CommandEnum.ready));
        //更新进度
        onProgress(session, task, 0);

    }

    /**
     *  计算上传速速, 并告知客户端
     * @param session session
     * @param task current upload  task
     * @param payloadLength 本次上传的字节大小
     */
    private void onProgress(WebSocketSession session, CurrentUploadTask task, int payloadLength) throws IOException {
        LOGGER.debug("progress: {}, {}", task.getCommandInfo().getFilename(), task.getWritePosition() + "/" + task.getCommandInfo().getTotal());
        UploadSpeed speed = (UploadSpeed) session.getAttributes().computeIfAbsent(UPLOAD_SPEED, k -> new UploadSpeed());
        String speedText = speed.click(payloadLength);
        ProgressInfo progressInfo = ProgressInfo.create(task.getCommandInfo().getTotal(), task.getWritePosition(), speedText);
        SocketResponse response = SocketResponse.success(CommandEnum.progress, progressInfo);
        sendMsg(session, response);


    }

    /**给客户端发送信息*/
    private void sendMsg(WebSocketSession session, SocketResponse msg) throws IOException {
        if (msg.getCode() != 0) {
            LOGGER.error("websocket上传文件出错了：{}", msg.getMsg());
        }
        String json = JsonUtil.toJson(msg);
        session.sendMessage(new TextMessage(json));
    }
}
